﻿using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Cli;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ServiceProxying;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Json;

using VoloGenerateProxyArgs = Volo.Abp.Cli.ServiceProxying.GenerateProxyArgs;

namespace LINGYUN.Abp.Cli.ServiceProxying.CSharp;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IServiceProxyGenerator), typeof(CSharpServiceProxyGenerator))]
public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase<CSharpServiceProxyGenerator>, ITransientDependency
{
    public const string Name = "CSHARP";

    private const string UsingPlaceholder = "<using placeholder>";
    private const string MethodPlaceholder = "<method placeholder>";
    private const string ClassName = "<className>";
    private const string ServiceInterface = "<serviceInterface>";
    private readonly static string[] ServicePostfixes = { "AppService", "ApplicationService" , "Service"};
    private const string DefaultNamespace = "ClientProxies";
    private const string Namespace = "<namespace>";
    private const string DefaultProvider = "ClientProxyBase";
    private const string Provider = "<provider>";
    private const string AppServicePrefix = "Volo.Abp.Application.Services";
    private readonly string _clientProxyGeneratedTemplate = "// This file is automatically generated by ABP framework to use MVC Controllers from CSharp" +
                                                   $"{Environment.NewLine}<using placeholder>" +
                                                   $"{Environment.NewLine}" +
                                                   $"{Environment.NewLine}// ReSharper disable once CheckNamespace" +
                                                   $"{Environment.NewLine}namespace <namespace>;" +
                                                   $"{Environment.NewLine}" +
                                                   $"{Environment.NewLine}[Dependency(ReplaceServices = true)]" +
                                                   $"{Environment.NewLine}[ExposeServices(typeof(<serviceInterface>), typeof(<className>))]" +
                                                   $"{Environment.NewLine}public partial class <className> : <provider><<serviceInterface>>, <serviceInterface>" +
                                                   $"{Environment.NewLine}{{" +
                                                   $"{Environment.NewLine}    <method placeholder>" +
                                                   $"{Environment.NewLine}}}" +
                                                   $"{Environment.NewLine}";
    private readonly string _clientProxyTemplate = "// This file is part of <className>, you can customize it here" +
                                                    $"{Environment.NewLine}// ReSharper disable once CheckNamespace" +
                                                    $"{Environment.NewLine}namespace <namespace>;" +
                                                    $"{Environment.NewLine}" +
                                                    $"{Environment.NewLine}public partial class <className>" +
                                                    $"{Environment.NewLine}{{" +
                                                    $"{Environment.NewLine}}}" +
                                                    $"{Environment.NewLine}";
    private readonly List<string> _usingNamespaceList = new()
    {
        "using System;",
        "using System.Threading.Tasks;",
        "using Volo.Abp.Application.Dtos;",
        "using Volo.Abp.Http.Client;",
        "using Volo.Abp.Http.Modeling;",
        "using Volo.Abp.DependencyInjection;",
        "using Volo.Abp.Http.Client.ClientProxying;",
        "using LINGYUN.Abp.Dapr;",
        "using LINGYUN.Abp.Dapr.Client;",
        "using LINGYUN.Abp.Dapr.Client.ClientProxying;"
    };

    public CSharpServiceProxyGenerator(
        CliHttpClientFactory cliHttpClientFactory,
        IJsonSerializer jsonSerializer) :
        base(cliHttpClientFactory, jsonSerializer)
    {
    }

    public async override Task GenerateProxyAsync(VoloGenerateProxyArgs args)
    {
        CheckWorkDirectory(args.WorkDirectory);
        CheckFolder(args.Folder);

        if (args.CommandName == RemoveProxyCommand.Name)
        {
            RemoveClientProxyFile(args);
            return;
        }

        var applicationApiDescriptionModel = await GetApplicationApiDescriptionModelAsync(
            args,
            new ApplicationApiDescriptionModelRequestDto
            {
                IncludeTypes = true
            });

        foreach (var controller in applicationApiDescriptionModel.Modules.Values.SelectMany(x => x.Controllers))
        {
            if (ShouldGenerateProxy(controller.Value))
            {
                await GenerateClientProxyFileAsync(args, controller.Value);
            }
        }

        await CreateGenerateProxyJsonFile(args, applicationApiDescriptionModel);
    }

    private async Task CreateGenerateProxyJsonFile(VoloGenerateProxyArgs args, ApplicationApiDescriptionModel applicationApiDescriptionModel)
    {
        var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;
        var filePath = Path.Combine(args.WorkDirectory, folder, $"{args.Module}-generate-proxy.json");

        using (var writer = new StreamWriter(filePath))
        {
            await writer.WriteAsync(JsonSerializer.Serialize(applicationApiDescriptionModel, indented: true));
        }
    }

    private void RemoveClientProxyFile(VoloGenerateProxyArgs args)
    {
        var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;
        var folderPath = Path.Combine(args.WorkDirectory, folder);

        if (Directory.Exists(folderPath))
        {
            Directory.Delete(folderPath, true);
        }

        Logger.LogInformation($"Delete {GetLoggerOutputPath(folderPath, args.WorkDirectory)}");
    }

    private async Task GenerateClientProxyFileAsync(
        VoloGenerateProxyArgs args,
        ControllerApiDescriptionModel controllerApiDescription)
    {
        var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;

        var appServiceTypeFullName = controllerApiDescription.Interfaces.Last().Type;
        var appServiceTypeName = appServiceTypeFullName.Split('.').Last();
        var clientProxyName = $"{controllerApiDescription.ControllerName}ClientProxy";
        var clientProvider = GetClientProxyProvider(args.As<GenerateProxyArgs>().Provider);
        var rootNamespace = $"{GetTypeNamespace(controllerApiDescription.Type)}.{folder.Replace('/', '.')}";
        var clientProxyBuilder = new StringBuilder(_clientProxyTemplate);
        clientProxyBuilder.Replace(ClassName, clientProxyName);
        clientProxyBuilder.Replace(Namespace, rootNamespace);
        clientProxyBuilder.Replace(Provider, clientProvider);

        var filePath = Path.Combine(args.WorkDirectory, folder, $"{clientProxyName}.cs");
        Directory.CreateDirectory(Path.GetDirectoryName(filePath));

        if (!File.Exists(filePath))
        {
            using (var writer = new StreamWriter(filePath))
            {
                await writer.WriteAsync(clientProxyBuilder.ToString());
            }

            Logger.LogInformation($"Create {GetLoggerOutputPath(filePath, args.WorkDirectory)}");
        }

        await GenerateClientProxyGeneratedFileAsync(
            args,
            controllerApiDescription,
            clientProxyName,
            appServiceTypeName,
            appServiceTypeFullName,
            rootNamespace,
            folder);
    }

    private async Task GenerateClientProxyGeneratedFileAsync(
        VoloGenerateProxyArgs args,
        ControllerApiDescriptionModel controllerApiDescription,
        string clientProxyName,
        string appServiceTypeName,
        string appServiceTypeFullName,
        string rootNamespace,
        string folder)
    {
        var clientProxyBuilder = new StringBuilder(_clientProxyGeneratedTemplate);

        var usingNamespaceList = new List<string>(_usingNamespaceList)
            {
                $"using {GetTypeNamespace(appServiceTypeFullName)};"
            };

        clientProxyBuilder.Replace(ClassName, clientProxyName);
        clientProxyBuilder.Replace(Namespace, rootNamespace);
        clientProxyBuilder.Replace(ServiceInterface, appServiceTypeName);

        foreach (var action in controllerApiDescription.Actions.Values)
        {
            if (!ShouldGenerateMethod(appServiceTypeFullName, action))
            {
                continue;
            }

            GenerateMethod(action, clientProxyBuilder, usingNamespaceList);
        }

        foreach (var usingNamespace in usingNamespaceList)
        {
            clientProxyBuilder.Replace($"{UsingPlaceholder}", $"{usingNamespace}{Environment.NewLine}{UsingPlaceholder}");
        }

        clientProxyBuilder.Replace($"{Environment.NewLine}{UsingPlaceholder}", string.Empty);
        clientProxyBuilder.Replace($"{Environment.NewLine}{Environment.NewLine}    {MethodPlaceholder}", string.Empty);

        var filePath = Path.Combine(args.WorkDirectory, folder, $"{clientProxyName}.Generated.cs");

        using (var writer = new StreamWriter(filePath))
        {
            await writer.WriteAsync(clientProxyBuilder.ToString());
            Logger.LogInformation($"Create {GetLoggerOutputPath(filePath, args.WorkDirectory)}");
        }
    }

    private void GenerateMethod(
        ActionApiDescriptionModel action,
        StringBuilder clientProxyBuilder,
        List<string> usingNamespaceList)
    {
        var methodBuilder = new StringBuilder();

        var returnTypeName = GetRealTypeName(action.ReturnValue.Type, usingNamespaceList);

        if (!action.Name.EndsWith("Async"))
        {
            GenerateSynchronizationMethod(action, returnTypeName, methodBuilder, usingNamespaceList);
            clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder}{Environment.NewLine}    {MethodPlaceholder}");
            return;
        }

        GenerateAsynchronousMethod(action, returnTypeName, methodBuilder, usingNamespaceList);
        clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder}{Environment.NewLine}    {MethodPlaceholder}");
    }

    private void GenerateSynchronizationMethod(ActionApiDescriptionModel action, string returnTypeName, StringBuilder methodBuilder, List<string> usingNamespaceList)
    {
        methodBuilder.AppendLine($"public virtual {returnTypeName} {action.Name}(<args>)");

        foreach (var parameter in action.Parameters.GroupBy(x => x.Name).Select(x => x.First()))
        {
            methodBuilder.Replace("<args>", $"{GetRealTypeName(parameter.Type, usingNamespaceList)} {parameter.Name}, <args>");
        }

        methodBuilder.Replace("<args>", string.Empty);
        methodBuilder.Replace(", )", ")");

        methodBuilder.AppendLine("    {");
        methodBuilder.AppendLine("        //Client Proxy does not support the synchronization method, you should always use asynchronous methods as a best practice");
        methodBuilder.AppendLine("        throw new System.NotImplementedException(); ");
        methodBuilder.AppendLine("    }");
    }

    private void GenerateAsynchronousMethod(
        ActionApiDescriptionModel action,
        string returnTypeName,
        StringBuilder methodBuilder,
        List<string> usingNamespaceList)
    {
        var returnSign = returnTypeName == "void" ? "Task" : $"Task<{returnTypeName}>";

        methodBuilder.AppendLine($"public async virtual {returnSign} {action.Name}(<args>)");

        foreach (var parameter in action.ParametersOnMethod)
        {
            methodBuilder.Replace("<args>", $"{GetRealTypeName(parameter.Type, usingNamespaceList)} {parameter.Name}, <args>");
        }

        methodBuilder.Replace("<args>", string.Empty);
        methodBuilder.Replace(", )", ")");

        methodBuilder.AppendLine("    {");

        var argsTemplate = "new ClientProxyRequestTypeValue" +
                   $"{Environment.NewLine}        {{<args>" +
                   $"{Environment.NewLine}        }}";

        var args = action.ParametersOnMethod.Any() ? argsTemplate : string.Empty;

        if (returnTypeName == "void")
        {
            methodBuilder.AppendLine($"        await RequestAsync(nameof({action.Name}), {args});");
        }
        else
        {
            methodBuilder.AppendLine($"        return await RequestAsync<{returnTypeName}>(nameof({action.Name}), {args});");
        }

        foreach (var parameter in action.ParametersOnMethod)
        {
            methodBuilder.Replace("<args>", $"{Environment.NewLine}            {{ typeof({GetRealTypeName(parameter.Type)}), {parameter.Name} }},<args>");
        }

        methodBuilder.Replace(",<args>", string.Empty);
        methodBuilder.Replace(", )", ")");
        methodBuilder.AppendLine("    }");
    }

    private bool ShouldGenerateProxy(ControllerApiDescriptionModel controllerApiDescription)
    {
        if (!controllerApiDescription.Interfaces.Any())
        {
            return false;
        }

        var serviceInterface = controllerApiDescription.Interfaces.Last();
        return ServicePostfixes.Any(x => serviceInterface.Type.EndsWith(x));
    }

    private bool ShouldGenerateMethod(string appServiceTypeName, ActionApiDescriptionModel action)
    {
        return action.ImplementFrom.StartsWith(AppServicePrefix) || action.ImplementFrom.StartsWith(appServiceTypeName);
    }

    private string GetTypeNamespace(string typeFullName)
    {
        return typeFullName.Substring(0, typeFullName.LastIndexOf('.'));
    }

    private string GetRealTypeName(string typeName, List<string> usingNamespaceList = null)
    {
        var filter = new[] { "<", ",", ">" };
        var stringBuilder = new StringBuilder();
        var typeNames = typeName.Split('.');

        if (typeNames.All(x => !filter.Any(x.Contains)))
        {
            if (usingNamespaceList != null)
            {
                AddUsingNamespace(usingNamespaceList, typeName);
            }

            return NormalizeTypeName(typeNames.Last());
        }

        var fullName = string.Empty;

        foreach (var item in typeNames)
        {
            if (filter.Any(x => item.Contains(x)))
            {
                if (usingNamespaceList != null)
                {
                    AddUsingNamespace(usingNamespaceList, $"{fullName}.{item}".TrimStart('.'));
                }

                fullName = string.Empty;

                if (item.Contains('<') || item.Contains(','))
                {
                    stringBuilder.Append(item.Substring(0, item.IndexOf(item.Contains('<') ? '<' : ',') + 1));
                    fullName = item.Substring(item.IndexOf(item.Contains('<') ? '<' : ',') + 1);
                }
                else
                {
                    stringBuilder.Append(item);
                }
            }
            else
            {
                fullName = $"{fullName}.{item}";
            }
        }

        return stringBuilder.ToString();
    }

    private void AddUsingNamespace(List<string> usingNamespaceList, string typeName)
    {
        var rootNamespace = $"using {GetTypeNamespace(typeName)};";
        if (usingNamespaceList.Contains(rootNamespace))
        {
            return;
        }

        usingNamespaceList.Add(rootNamespace);
    }

    private static string GetClientProxyProvider(string provider)
    {
        return provider switch
        {
            "dapr" => "DaprClientProxyBase",
            "http" => "ClientProxyBase",
            _ => "ClientProxyBase"
        };
    }

    private string NormalizeTypeName(string typeName)
    {
        var nullable = string.Empty;
        if (typeName.EndsWith("?"))
        {
            typeName = typeName.TrimEnd('?');
            nullable = "?";
        }

        typeName = typeName switch
        {
            "Void" => "void",
            "Boolean" => "bool",
            "String" => "string",
            "Int32" => "int",
            "Int64" => "long",
            "Double" => "double",
            "Object" => "object",
            "Byte" => "byte",
            "Char" => "char",
            _ => typeName
        };

        return $"{typeName}{nullable}";
    }

    private void CheckWorkDirectory(string directory)
    {
        if (!Directory.Exists(directory))
        {
            throw new CliUsageException("Specified directory does not exist.");
        }

        var projectFiles = Directory.GetFiles(directory, "*.csproj");
        if (!projectFiles.Any())
        {
            throw new CliUsageException("No project file(csproj) found in the directory.");
        }
    }

    private void CheckFolder(string folder)
    {
        if (!folder.IsNullOrWhiteSpace() && Path.HasExtension(folder))
        {
            throw new CliUsageException("Option folder should be a directory.");
        }
    }

    protected override ServiceType? GetDefaultServiceType(VoloGenerateProxyArgs args)
    {
        return ServiceType.Application;
    }
}
